44  数据结构之字典

本章定位(复习与补充训练层):本章对应正课第2章《列表、字典和元组》(章节 2)的字典部分,并与第37章《数据基本结构(元组、列表、字典)》(章节 38)互为补充训练(本章为分项版),用于复习、补缺与额外练习。建议先不看讲解,直接尝试下方平台任务,再对照解析补弱项。本章不属于必修主线。先做本章『动手与思考』第 1 题与平台任务自测,通过即可跳过本章。

44.1 引言字典的键值对与哈希表

字典(Dict)按键存取值:键唯一且必须是不可变类型,平均查找复杂度为O(1),是”股票代码到名称”“日期到行情”这类映射关系的首选结构。本章用一个含四个小任务的平台代码块,检验你对字典创建、查询、修改与增删的复习效果。

44.2 本章学习目标

本章为复习与补充训练,目标是检验你是否能脱离正课独立完成下列任务:

  1. 能独立完成平台任务一:创建四个汇率信息字典,分别输出 keys()values()items()
  2. 能独立完成平台任务二:用键直接查询 dict1["日期"]dict2["中间价"]dict4["涨跌幅"]
  3. 能独立完成平台任务三:按键修改已有键值对(dict2["日期"] = "2024-08-29" 等)后输出整个字典
  4. 能独立完成平台任务四:用 update() 批量新增”前一日中间价”“前一日涨跌幅”,并用 del dict4["涨跌幅"] 删除键
  5. 能说出 d[key]d.get(key, default) 的区别,并写出基本的字典推导式

若上述任务无法独立完成,请回到正课第2章字典部分补弱,再回到本章练习。

44.3 字典的创建

先概括字典的四大核心特性,创建与操作方式都由此决定:

字典的四大核心特性:

特性 技术实现 时间复杂度 金融应用场景
键唯一性 哈希表约束 - 股票代码映射
快速查找 哈希计算 O(1)平均 实时行情查询
有序性(Python 3.7+) 维护插入顺序 O(1) 时间序列索引
异构性 键值可变类型 - 多维度数据

平台任务(平台原始代码)

以下代码与教学平台任务要求完全一致:

任务要求(单块含任务一至任务四):给定四个汇率信息字典 dict1~dict4(键均为”汇率变量”“日期”“中间价”“涨跌幅”)。 - 任务一:分别输出 dict1.keys()dict2.values()dict3.items(); - 任务二:用键直接查询并输出 dict1["日期"]dict2["中间价"]dict4["涨跌幅"]; - 任务三:按键修改 dict2 的日期、中间价、涨跌幅三个键值对后输出整个字典; - 任务四:用 update()dict3 批量新增”前一日中间价”“前一日涨跌幅”,再用 del dict4["涨跌幅"] 删除键后输出。 请按平台原始题面原样输入(注释除外),判定以平台为准。

列表 44.1: 平台原始代码 (1)-(4)
# 注:平台题面为填空形式,下方展示的是填空完成后的参考形态;请按平台原始题面原样输入,以平台判定为准
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
#任务一
dict1 = {"汇率变量":"美元兑人民币","日期":"2024-08-30","中间价":"7.1124","涨跌幅":"-0.0025"}  
dict2 = {"汇率变量":"欧元兑人民币","日期":"2024-08-21","中间价":"7.9325","涨跌幅":"0.0038"}  # 定义字典dict2
dict3 = {"汇率变量":"港元兑人民币","日期":"2024-08-19","中间价":"0.9162","涨跌幅":"-0.0003"}  # 定义字典dict3
dict4 = {"汇率变量":"英镑兑人民币","日期":"2024-08-08","中间价":"9.0701","涨跌幅":"0.0011"}  # 定义字典dict4

print(dict1.keys()) #输出字典dict1的全部键码

print(dict2.values()) #输出字典dict2的全部数值

print(dict3.items())  #输出字典dict3的全部元素


#任务二
dict1 = {"汇率变量":"美元兑人民币","日期":"2024-08-30","中间价":"7.1124","涨跌幅":"-0.0025"}  
dict2 = {"汇率变量":"欧元兑人民币","日期":"2024-08-21","中间价":"7.9325","涨跌幅":"0.0038"}  # 定义字典dict2
dict4 = {"汇率变量":"英镑兑人民币","日期":"2024-08-08","中间价":"9.0701","涨跌幅":"0.0011"}  # 定义字典dict4
 
print(dict1["日期"])   #查询美元兑人民币汇率的具体日期
 
print(dict2["中间价"])  #查询欧元兑人民币汇率的中间价
 
print(dict4["涨跌幅"])   #查询英镑兑人民币的涨跌幅
 
#任务三
dict2 = {"汇率变量":"欧元兑人民币","日期":"2024-08-21","中间价":"7.9325","涨跌幅":"0.0038"}
 
dict2["日期"] = "2024-08-29"           #修改字典2新的日期

dict2["中间价"] = 7.9318               #修改字典2新的中间价

dict2["涨跌幅"] = -0.0037              #修改字典2新的涨跌幅

print(dict2)  # 输出变量dict2的值

#任务四
dict3 = {"汇率变量":"港元兑人民币","日期":"2024-08-19","中间价":"0.9162","涨跌幅":"-0.0003"}

#使用update函数在dict3中添加"前一日中间价":"0.9165","前一日涨跌幅":"0.0002"
dict3.update({"前一日中间价":"0.9165","前一日涨跌幅":"0.0002"})   
print(dict3)  # 输出任务四
 
dict4 = {"汇率变量":"英镑兑人民币","日期":"2024-08-08","中间价":"9.0701","涨跌幅":"0.0011"}  # 定义字典dict4
del dict4["涨跌幅"]           #删除dict4的涨跌幅
print(dict4)  # 输出变量dict4的值

预期输出(本机实际运行结果,具体以平台运行结果为准):

dict_keys(['汇率变量', '日期', '中间价', '涨跌幅'])
dict_values(['欧元兑人民币', '2024-08-21', '7.9325', '0.0038'])
dict_items([('汇率变量', '港元兑人民币'), ('日期', '2024-08-19'), ('中间价', '0.9162'), ('涨跌幅', '-0.0003')])
2024-08-30
7.9325
0.0011
{'汇率变量': '欧元兑人民币', '日期': '2024-08-29', '中间价': 7.9318, '涨跌幅': -0.0037}
{'汇率变量': '港元兑人民币', '日期': '2024-08-19', '中间价': '0.9162', '涨跌幅': '-0.0003', '前一日中间价': '0.9165', '前一日涨跌幅': '0.0002'}
{'汇率变量': '英镑兑人民币', '日期': '2024-08-08', '中间价': '9.0701'}
列表 44.2: 字典的多种创建方式与股票代码映射
# 方式1:字面量语法(推荐)
# 花括号{}是字典的字面量语法
# 键值对格式: 键:值,使用冒号分隔
stock_codes_a = {
    '600': '上证A股',
    '900': '上证B股',
    '000': '深证A股',
    '200': '深证B股',
    '400': '三板市场股票'
}

# 方式2:dict()构造函数
# 先创建空字典,然后逐个添加键值对
stock_codes_b = dict()  # 创建空字典
stock_codes_b['600'] = '上证A股'  # 添加键值对
stock_codes_b['900'] = '上证B股'
stock_codes_b['000'] = '深证A股'
stock_codes_b['200'] = '深证B股'
stock_codes_b['400'] = '三板市场股票'

# 方式3:dict.fromkeys()批量创建
# 适用于创建所有值相同的字典
codes = ['600', '900', '000']  # 键列表
default_value = '其他'  # 默认值
stock_codes_c = dict.fromkeys(codes, default_value)

# 打印字典内容
# 字典的字符串表示格式为 {键: 值, ...}
print('字典A:', stock_codes_a)

# 通过键访问值
# stock_codes_a['000']获取键'000'对应的值
print(f"\n'000'对应: {stock_codes_a['000']}")  # 输出: 深证A股

代码深度解析:

  1. 字典的内存结构:

    # CPython 3.8+的字典使用紧凑表(Compact Table)
    # 每个条目占用24字节:
    # - 哈希值: 8字节
    # - 键指针: 8字节
    # - 值指针: 8字节
    
    # 对于5个键值对的字典:
    # 内存占用 ≈ 5 × 24 = 120字节 + 开销
  2. 哈希冲突的实际影响:

    # 演示哈希冲突
    # 假设两个不同的键产生相同的哈希值(简化示例)
    
    dict1 = {}
    dict1['apple'] = 1
    # 假设'banana'的哈希值与'apple'冲突
    dict1['banana'] = 2  # Python会自动处理冲突
    
    # 对用户透明,无需关心内部实现
    print(dict1)  # {'apple': 1, 'banana': 2}
  3. 创建方式的性能对比:

    import time
    
    # 方式1:字面量(最快)
    start = time.time()
    for _ in range(100000):
        d = {'a': 1, 'b': 2, 'c': 3}
    print(f'字面量: {time.time() - start:.4f}秒')
    
    # 方式2:dict()构造函数
    start = time.time()
    for _ in range(100000):
        d = dict(a=1, b=2, c=3)
    print(f'dict(): {time.time() - start:.4f}秒')
  4. 金融应用:股票代码分类:

    # 中国股票代码规则:
    # - 600xxx, 601xxx, 603xxx: 上交所主板
    # - 300xxx: 深交所创业板
    # - 688xxx: 上交所科创板
    
    code_classification = {
        '600': '上交所主板',
        '601': '上交所主板',
        '603': '上交所主板',
        '300': '创业板',
        '688': '科创板'
    }
    
    # 查询股票类别
    def classify_stock(code):
        prefix = code[:3]  # 获取前3位
        return code_classification.get(prefix, '未知类别')
    
    print(classify_stock('600519'))  # 上交所主板
    print(classify_stock('300750'))  # 创业板

44.4 字典的操作

字典的增删查改为何能保持平均O(1)?答案在它底层的哈希表实现:

理论背景:哈希表的实现原理

Python字典(Dict)是基于哈希表(Hash Table)实现的关联数据结构。从计算机科学的角度来看,字典是抽象数据类型(Abstract Data Type)Map的一种高效实现:

  1. 键值对映射(Key-Value Mapping): 每个键(Key)唯一对应一个值(Value)
  2. 哈希函数(Hash Function): hash(key) → 整数索引,将键转换为数组索引
  3. 冲突解决(Collision Resolution): 处理多个键映射到同一索引的情况

哈希函数本身的性质决定了冲突处理的代价:

数学分析:哈希函数的设计

理想哈希函数应该满足:

\[ \forall key_1 \neq key_2: hash(key_1) \neq hash(key_2) \]

但实际上,由于键空间远大于数组大小,冲突不可避免。Python使用以下策略:

  1. 开放寻址法(Open Addressing): 当发生冲突时,寻找下一个空闲槽位
  2. 探测序列(Probe Sequence): \(h(k, i) = (h_1(k) + i \cdot h_2(k)) \mod m\) 其中\(i\)是探测次数,\(h_1\)\(h_2\)是两个哈希函数

时间复杂度分析:

操作 平均情况 最坏情况 说明
插入 O(1) O(n) 负载因子< 0.66时
查找 O(1) O(n) 同上
删除 O(1) O(n) 同上

Python字典保持负载因子(Load Factor)低于2/3,确保平均性能为O(1)。

补充说明:为什么字典查找比列表快数百倍?

假设有1000只股票:

  • 列表查找: 平均需要500次比较(O(n))
  • 字典查找: 只需1次哈希计算(O(1))

性能差异: 500倍!这正是高频交易系统使用字典存储订单簿的原因。

列表 44.3: 字典的常用操作与股票查询
# 创建股票代码到名称的映射
# 键是股票代码(字符串),值是股票名称(字符串)
stock_dict = {
    '600519.SH': '贵州茅台',
    '000858.SZ': '五粮液',
    '600036.SH': '招商银行'
}

# 访问元素:使用get()方法(推荐)
# get()方法在键不存在时返回默认值,不会报错
# 语法: dict.get(key, default_value)
print('查询600519.SH:', stock_dict.get('600519.SH', '未知'))
# 输出: 贵州茅台

# 查询不存在的键
print('查询999999.XX:', stock_dict.get('999999.XX', '未知'))
# 输出: 未知 (如果用stock_dict['999999.XX']会报KeyError)

# 添加新键值对
# 如果键已存在,会覆盖原值
stock_dict['601318.SH'] = '中国平安'

# 删除键值对
# del语句会从字典中移除键,如果键不存在会报KeyError
del stock_dict['600036.SH']  # 删除'招商银行'

# 获取所有键
# keys()返回dict_keys对象,需要转换为列表查看
print(f'\n键: {list(stock_dict.keys())}')
# 输出: ['600519.SH', '000858.SZ', '601318.SH']

# 获取所有值
# values()返回dict_values对象
print(f'值: {list(stock_dict.values())}')
# 输出: ['贵州茅台', '五粮液', '中国平安']

# 获取字典大小
# len()函数返回键值对的数量
print(f'项数: {len(stock_dict)}')  # 输出: 3

代码深度解析:

  1. 访问方式的对比:

    # 方式1:直接访问(键不存在时报错)
    try:
        name = stock_dict['999999.XX']
    except KeyError:
        print('键不存在!')
        name = None
    
    # 方式2:get()方法(键不存在时返回默认值)
    name = stock_dict.get('999999.XX', None)
    
    # 方式2更安全,推荐使用
  2. 删除操作的安全性:

    # 方法1:del语句(键不存在时报错)
    # del stock_dict['999999.XX']  # KeyError!
    
    # 方法2:pop()方法(键不存在时返回默认值)
    removed = stock_dict.pop('999999.XX', None)
    print(removed)  # None (不会报错)
    
    # 方法3:popitem()删除并返回最后一个键值对
    # Python 3.7+中,字典保持插入顺序
    key, value = stock_dict.popitem()
    print(f'删除了: {key} -> {value}')
  3. 字典更新操作:

    # update()方法:批量更新字典
    new_stocks = {
        '000001.SZ': '平安银行',
        '601318.SH': '中国平安(更新)'
    }
    stock_dict.update(new_stocks)
    # 如果键已存在,会更新值
    # 如果键不存在,会添加新键值对
  4. 金融应用:实时行情更新:

    # 模拟实时行情更新
    quotes = {
        '600519.SH': 1850.00,
        '000858.SZ': 220.50
    }
    
    # 更新价格
    def update_price(code, new_price):
        if code in quotes:  # 检查键是否存在
            old_price = quotes[code]
            quotes[code] = new_price
            change = (new_price - old_price) / old_price
            print(f'{code} 更新: {old_price:.2f} -> {new_price:.2f} ({change:+.2%})')
        else:
            quotes[code] = new_price
            print(f'{code} 新增: {new_price:.2f}')
    
    update_price('600519.SH', 1860.00)  # 更新
    update_price('000001.SZ', 18.50)    # 新增

44.5 字典的遍历

遍历顺序与字典”是否有序”直接相关,先辨析一个常见的版本疑问:

易混淆概念辨析:Python 3.6 vs 3.7的字典有序性

  • Python 3.6: 字典有序是实现细节(CPython的副作用)
  • Python 3.7+: 字典有序成为语言规范(官方保证)

因此,在Python 3.7+中,可以放心依赖字典的插入顺序。

列表 44.4: 字典的遍历方式与字典推导式
# 创建股票价格字典
# 键是股票名称,值是股票价格
prices = {'贵州茅台': 1850, '五粮液': 220, '招商银行': 45}

# 方式1:遍历键
# for key in dict: 等价于 for key in dict.keys():
print('遍历键:')
for key in prices:
    print(f'  {key}')
# 输出:
#   贵州茅台
#   五粮液
#   招商银行

# 方式2:遍历键值对
# items()方法返回(键, 值)元组的迭代器
print('\n遍历键值对:')
for key, value in prices.items():
    # 同时解包键和值
    print(f'  {key}: {value}元')
# 输出:
#   贵州茅台: 1850元
#   五粮液: 220元
#   招商银行: 45元

# 方式3:遍历值
# values()方法返回值的迭代器
print('\n遍历值:')
for value in prices.values():
    print(f'  {value}元')

# 字典推导式:创建新字典的优雅方式
# 语法: {键表达式: 值表达式 for item in iterable}
# 下面的代码将字典的键值对反转
inverted = {v: k for k, v in prices.items()}
print(f'\n反转字典: {inverted}')
# 输出: {1850: '贵州茅台', 220: '五粮液', 45: '招商银行'}

# 注意:如果原字典的值不唯一,反转会丢失数据!
# 因为字典的键必须唯一

代码深度解析:

  1. 遍历方式的性能对比:

    import time
    
    large_dict = {i: i**2 for i in range(1000000)}
    
    # 方式1:遍历键
    start = time.time()
    for key in large_dict:
        _ = large_dict[key]
    print(f'遍历键: {time.time() - start:.4f}秒')
    
    # 方式2:遍历键值对
    start = time.time()
    for key, value in large_dict.items():
        _
    print(f'遍历键值对: {time.time() - start:.4f}秒')
    
    # 方式2稍快,因为避免了额外的哈希查找
  2. 字典推导式的应用:

    # 应用1:过滤字典
    # 找出价格大于100的股票
    expensive = {k: v for k, v in prices.items() if v > 100}
    print(expensive)  # {'贵州茅台': 1850, '五粮液': 220}
    
    # 应用2:转换字典值
    # 将价格转换为万元
    prices_wan = {k: v/10000 for k, v in prices.items()}
    print(prices_wan)
    # {'贵州茅台': 0.185, '五粮液': 0.022, '招商银行': 0.0045}
    
    # 应用3:合并两个字典
    dict1 = {'a': 1, 'b': 2}
    dict2 = {'c': 3, 'd': 4}
    merged = {**dict1, **dict2}  # Python 3.5+的解包语法
    print(merged)  # {'a': 1, 'b': 2, 'c': 3, 'd': 4}
  3. 金融应用:计算投资组合权重:

    # 持仓价值
    holdings = {
        '贵州茅台': 185000,
        '五粮液': 44000,
        '招商银行': 22500
    }
    
    total = sum(holdings.values())
    
    # 计算权重
    weights = {stock: value/total for stock, value in holdings.items()}
    
    print('持仓权重:')
    for stock, weight in sorted(weights.items(), key=lambda x: -x[1]):
        print(f'  {stock}: {weight:.2%}')
  4. 内存视图:字典的内部结构:

    # 查看字典的内存占用
    import sys
    
    small_dict = {'a': 1, 'b': 2}
    print(f'字典大小: {sys.getsizeof(small_dict)}字节')
    
    # 字典包含多个部分:
    # - 哈希表(主要部分)
    # - 条目数组(存储键值对)
    # - 元数据(大小、版本等)

44.6 金融应用股票代码映射

列表 44.5: 字典在股票代码查询中的应用
# 股票代码到名称的映射
# 这是一个典型的"查找表"(Lookup Table)应用场景
# 字典的O(1)查找性能使得它非常适合此类应用
code_to_name = {
    '600519.SH': '贵州茅台',
    '000858.SZ': '五粮液',
    '600036.SH': '招商银行',
    '601318.SH': '中国平安',
    '000001.SZ': '平安银行'
}

# 批量查询的股票代码列表
codes_to_query = ['600519.SH', '000858.SZ', '000001.SZ']

# 遍历并查询
print('股票查询结果:')
for code in codes_to_query:
    # 使用get()方法,如果代码不存在返回'未知股票'
    # 这避免了KeyError异常
    name = code_to_name.get(code, '未知股票')
    print(f'{code}: {name}')

# 反向映射:从名称到代码
# 使用字典推导式交换键和值
# 注意:如果名称不唯一,会丢失数据!
name_to_code = {v: k for k, v in code_to_name.items()}

# 查询平安银行的代码
# 使用get()方法,如果名称不存在返回None
print(f'\n平安银行代码: {name_to_code.get("平安银行")}')
# 输出: 000001.SZ

# 查询不存在的名称
print(f'腾讯代码: {name_to_code.get("腾讯")}')
# 输出: None (不会报错)

代码深度解析:

  1. 双向映射的设计模式:

    # 更健壮的双向映射实现
    class BidirectionalMap:
        def __init__(self):
            self.key_to_value = {}
            self.value_to_key = {}
    
        def add(self, key, value):
            # 检查值是否已存在
            if value in self.value_to_key:
                raise ValueError(f'值{value}已存在!')
    
            self.key_to_value[key] = value
            self.value_to_key[value] = key
    
        def get_key(self, value):
            return self.value_to_key.get(value)
    
        def get_value(self, key):
            return self.key_to_value.get(key)
    
    # 使用示例
    bm = BidirectionalMap()
    bm.add('600519.SH', '贵州茅台')
    print(bm.get_key('贵州茅台'))  # 600519.SH
    print(bm.get_value('600519.SH'))  # 贵州茅台
  2. 默认字典的应用:

    from collections import defaultdict
    
    # 使用defaultdict避免手动检查键是否存在
    # 当访问不存在的键时,自动创建默认值
    counter = defaultdict(int)  # 默认值为0
    
    # 统计股票出现次数
    codes = ['600519.SH', '000858.SZ', '600519.SH', '600519.SH']
    for code in codes:
        counter[code] += 1  # 即使code不存在,也不会报错
    
    print(counter)  # defaultdict(<class 'int'>, {'600519.SH': 3, '000858.SZ': 1})
  3. 嵌套字典的应用:

    # 存储股票的多维度信息
    stock_info = {
        '600519.SH': {
            'name': '贵州茅台',
            'price': 1850.00,
            'pe': 45.2,
            'market_cap': 2325000000000  # 市值(元)
        },
        '000858.SZ': {
            'name': '五粮液',
            'price': 220.50,
            'pe': 32.8,
            'market_cap': 856000000000
        }
    }
    
    # 访问嵌套信息
    maotai = stock_info['600519.SH']
    print(f'{maotai["name"]}: PE={maotai["pe"]}, 市值={maotai["market_cap"]/1e8:.0f}亿元')
  4. OrderedDict的应用:

    from collections import OrderedDict
    
    # OrderedDict在Python 3.7+之前很有用
    # 在Python 3.7+中,普通dict已经有序
    
    # 维护时间序列数据
    time_series = OrderedDict()
    time_series['2024-01-01'] = 1850.0
    time_series['2024-01-02'] = 1860.5
    time_series['2024-01-03'] = 1855.0
    
    # 获取最近的价格
    latest_date = next(reversed(time_series))
    latest_price = time_series[latest_date]
    print(f'最新价格({latest_date}): {latest_price}')

性能优化技巧:

  1. 预分配字典大小(如果可能):

    # 对于已知大小的字典,预分配可以提高性能
    d = {}
    d.update({i: i**2 for i in range(1000)})
    # 比逐个插入更快
  2. 使用__slots__减少内存:

    class Stock:
        __slots__ = ['code', 'name', 'price']
        def __init__(self, code, name, price):
            self.code = code
            self.name = name
            self.price = price
    
    # 使用对象而非字典可以减少内存占用
  3. 避免频繁的字典操作:

    # 不推荐
    for stock in stocks:
        if stock['code'] not in cache:
            cache[stock['code']] = {}
        cache[stock['code']]['price'] = stock['price']
    
    # 推荐
    for stock in stocks:
        if stock['code'] in cache:
            cache[stock['code']]['price'] = stock['price']
        else:
            cache[stock['code']] = {'price': stock['price']}

最佳实践总结:

  1. 使用字典的场景:

    • 需要通过键快速查找值
    • 键是字符串或数字等不可变类型
    • 需要维护键值对关系
    • 数据需要频繁查询
  2. 避免字典的场景:

    • 需要保持顺序(Python 3.6-)
    • 需要序列化(考虑使用OrderedDict)
    • 键是可变类型(如列表)
  3. 字典vs列表的选择决策树:

    需要通过特定标识符查找数据?
    ├─ 是 → 使用字典
    └─ 否 → 需要按顺序访问?
        ├─ 是 → 使用列表
        └─ 否 → 使用集合(去重)

44.7 本章小结

要点:

  • 字典按键存取,键唯一且必须是不可变类型;Python 3.7+ 的字典保持插入顺序
  • 创建方式:字面量 {} 一次成型、dict() 加逐个赋值动态构建、dict.fromkeys() 批量建同值字典
  • 增改:d[k] = v(键存在则覆盖)、update() 批量增改;删除:del d[k]pop()popitem()
  • keys()values()items() 返回视图对象,可直接迭代与做成员判断,查看完整内容需 list()
  • 三种遍历方式(键、键值对、值);字典推导式 {v: k for k, v in d.items()} 可反转字典,但值不唯一时会丢数据

易错点:

  • d[key] 访问不存在的键会抛 KeyError,稳妥写法是 d.get(key, 默认值)
  • update() 对已存在的键执行的是覆盖,不是追加
  • 平台任务三把”中间价”从字符串 '7.9325' 改成了数值 7.9318,值的类型随之改变;后续做数值运算前要先统一类型
  • 用列表等可变对象作键会抛 TypeError

44.8 动手与思考

以下练习每题附参考答案(默认折叠)。请先独立完成并写下你的判断,再点开对照,最后上机验证。

  1. 概念自测(不看正课):keys()values()items() 各返回什么?为什么字典查找平均是 O(1)?字典的值不唯一时用推导式反转会发生什么?

    参考答案(点开前请先独立完成)

    解题思路keys()values()items() 分别返回字典的键视图、值视图、键值对视图(items() 的元素是 (键, 值) 元组);视图是动态的,随字典增删自动更新,常配 list() 或 for 循环使用。平均 O(1) 的原因:字典底层是哈希表,对键计算哈希值即可直接定位存储位置,查找代价与字典大小无关(不理想时退化为 O(n),但平均是常数级)。值不唯一时用 {v: k for k, v in d.items()} 反转,重复的值作为新键会相互覆盖,最终只保留遍历中最后出现的那个键值对——反转前应先确认值唯一。

    回扣主线章:字典的创建、访问与视图的讲解见第 章节 2 章。

  2. 输出预测:不运行代码,先写出下面代码的输出结果,再上机检验你的判断。

    d = {'汇率变量': '美元兑人民币', '日期': '2024-08-30', '中间价': '7.1124'}
    print(list(d.keys()))
    print(d['中间价'])
    d['涨跌幅'] = '-0.0025'
    d.update({'前一日中间价': '7.1131'})
    print(len(d))
    print(d.get('前一日涨跌幅', '无数据'))

    参考答案(先写下你的预测再点开)

    解题思路:初始 d 有 3 个键,list(d.keys()) 输出 ['汇率变量', '日期', '中间价']d['中间价'] 按键取值输出 7.1124(值本身是字符串,打印不带引号);d['涨跌幅'] = '-0.0025' 新增一个键、d.update(...) 再新增“前一日中间价”一个键,此时共 5 个键,len(d) 输出 5d.get('前一日涨跌幅', '无数据') 查询的键不存在,返回默认值 无数据(用 d[键] 则会抛 KeyError,这正是 get 的稳妥之处)。

    # 验证脚本:原样运行题面代码
    d = {'汇率变量': '美元兑人民币', '日期': '2024-08-30', '中间价': '7.1124'}  # 汇率信息字典
    print(list(d.keys()))  # 输出全部键组成的列表
    print(d['中间价'])  # 按键查询中间价
    d['涨跌幅'] = '-0.0025'  # 新增键值对
    d.update({'前一日中间价': '7.1131'})  # 批量更新(此处为新增)
    print(len(d))  # 输出键值对个数
    print(d.get('前一日涨跌幅', '无数据'))  # 键不存在时返回默认值

    预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):

    ['汇率变量', '日期', '中间价']
    7.1124
    5
    无数据

    回扣主线章:键查询与 get() 的稳妥写法见第 章节 2 章。

  3. 动手验证:先执行 del d['日期'] 并输出字典,确认键值对已被删除;再尝试 d[['a', 'b']] = 1,捕获错误并说出字典键必须满足什么要求。

    参考答案(点开前请先独立完成)

    解题思路del d['日期'] 把“日期”键及其值一起删除(不是置空,是键值对整体消失),输出可见该键已不在。d[['a', 'b']] = 1TypeError: unhashable type: 'list'——字典的键必须是可哈希的不可变类型(字符串、数值、元组等);列表可变、不可哈希,无法作为键。若确实要用多个字段作键,应改用元组 d[('a', 'b')] = 1

    # 验证脚本:删除键值对,以及不可哈希类型作键的报错
    d = {'汇率变量': '美元兑人民币', '日期': '2024-08-30', '中间价': '7.1124', '涨跌幅': '-0.0025'}  # 含日期键的汇率字典
    del d['日期']  # 删除日期键及其值
    print(d)  # 输出删除后的字典,日期键已消失
    try:
        d[['a', 'b']] = 1  # 尝试用列表作键(预期报错)
    except TypeError as error_msg:
        print('错误类型:', type(error_msg).__name__, '|', error_msg)  # 捕获并输出错误

    预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):

    {'汇率变量': '美元兑人民币', '中间价': '7.1124', '涨跌幅': '-0.0025'}
    错误类型: TypeError | unhashable type: 'list'

    回扣主线章del 删除键值对与键的可哈希要求见第 章节 2 章。

  4. 变式任务(平台任务三、四同型改造):仿照任务三把”日期”改为下一个交易日,仿照任务四用一次 update() 同时新增”开盘价”与”最高价”两个键,最后 del 掉”涨跌幅”并输出最终字典。

    参考答案(点开前请先独立完成)

    解题思路:改值用 d[已有键] = 新值,一次只改一个键;批量新增用一次 update() 传入含两个键值对的字典;删除用 del d[键]。三步之后字典剩“汇率变量、日期、中间价、开盘价、最高价”五个键。

    # 变式程序:改值、批量新增、删除三步维护汇率字典
    rate_dict = {'汇率变量': '美元兑人民币', '日期': '2024-08-30', '中间价': '7.1124', '涨跌幅': '-0.0025'}  # 初始字典
    rate_dict['日期'] = '2024-09-02'  # 仿照任务三:把日期改为下一个交易日
    rate_dict.update({'开盘价': '7.1100', '最高价': '7.1188'})  # 仿照任务四:一次update新增两个键
    del rate_dict['涨跌幅']  # 删除涨跌幅键值对
    print(rate_dict)  # 输出最终字典

    预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):

    {'汇率变量': '美元兑人民币', '日期': '2024-09-02', '中间价': '7.1124', '开盘价': '7.1100', '最高价': '7.1188'}

    注意:以上为本题变式的独立代码;列表 44.1 对应平台任务的原始代码块仍须原样输入教学平台,不要用本变式替换。

    回扣主线章:字典改值、update()del 的同型操作见第 章节 2 章。